﻿// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Diagnostics;
using Mono.Cecil;
using Mono.Cecil.Cil;
using Mono.Collections.Generic;

namespace Mono.Linker.Steps
{

    public class AddBypassNGenStep : BaseStep
    {

        AssemblyDefinition? coreLibAssembly;
        CustomAttribute? bypassNGenAttribute;

        protected override void ProcessAssembly(AssemblyDefinition assembly)
        {
            if (Annotations.GetAction(assembly) == AssemblyAction.AddBypassNGen)
            {
                coreLibAssembly = Context.Resolve(assembly.MainModule.TypeSystem.CoreLibrary);
                if (coreLibAssembly == null)
                    return;
                bypassNGenAttribute = null;
                if (assembly == coreLibAssembly)
                {
                    EnsureBypassNGenAttribute(assembly.MainModule);
                }

                foreach (var type in assembly.MainModule.Types)
                    ProcessType(type);
            }
        }

        void ProcessType(TypeDefinition type)
        {
            if (type.HasMethods)
                ProcessMethods(type.Methods, type.Module);

            if (type.HasNestedTypes)
                ProcessNestedTypes(type);
        }

        void ProcessMethods(Collection<MethodDefinition> methods, ModuleDefinition module)
        {
            foreach (var method in methods)
                if (!Annotations.IsMarked(method))
                {
                    EnsureBypassNGenAttribute(module);
                    method.CustomAttributes.Add(bypassNGenAttribute);
                }
        }

        void ProcessNestedTypes(TypeDefinition type)
        {
            for (int i = 0; i < type.NestedTypes.Count; i++)
            {
                var nested = type.NestedTypes[i];
                ProcessType(nested);
            }
        }

        private void EnsureBypassNGenAttribute(ModuleDefinition targetModule)
        {
            Debug.Assert(coreLibAssembly != null);
            if (bypassNGenAttribute != null)
            {
                return;
            }
            ModuleDefinition corelibMainModule = coreLibAssembly.MainModule;
            TypeReference bypassNGenAttributeRef = new TypeReference("System.Runtime", "BypassNGenAttribute", corelibMainModule, targetModule.TypeSystem.CoreLibrary);
            TypeDefinition bypassNGenAttributeDef = corelibMainModule.MetadataResolver.Resolve(bypassNGenAttributeRef);
            MethodDefinition? bypassNGenAttributeDefaultConstructor = null;

            if (bypassNGenAttributeDef == null)
            {
                // System.Runtime.BypassNGenAttribute is not found in corelib. Add it.
                TypeReference systemAttributeRef = new TypeReference("System", "Attribute", corelibMainModule, targetModule.TypeSystem.CoreLibrary);
                TypeReference systemAttribute = corelibMainModule.MetadataResolver.Resolve(systemAttributeRef);
                systemAttribute = corelibMainModule.ImportReference(systemAttribute);

                if (systemAttribute == null)
                    throw new System.ApplicationException("System.Attribute is not found in " + targetModule.TypeSystem.CoreLibrary.Name);

                MethodReference systemAttributeDefaultConstructorRef = new MethodReference(".ctor", corelibMainModule.TypeSystem.Void, systemAttributeRef);
                MethodReference systemAttributeDefaultConstructor = corelibMainModule.MetadataResolver.Resolve(systemAttributeDefaultConstructorRef);
                systemAttributeDefaultConstructor = corelibMainModule.ImportReference(systemAttributeDefaultConstructor);

                if (systemAttributeDefaultConstructor == null)
                    throw new System.ApplicationException("System.Attribute has no default constructor");

                bypassNGenAttributeDef = new TypeDefinition("System.Runtime", "BypassNGenAttribute", TypeAttributes.NotPublic | TypeAttributes.Sealed, systemAttribute);

                coreLibAssembly.MainModule.Types.Add(bypassNGenAttributeDef);

                if (Annotations.GetAction(coreLibAssembly) == AssemblyAction.Copy)
                {
                    Annotations.SetAction(coreLibAssembly, AssemblyAction.Save);
                }

                const MethodAttributes ctorAttributes = MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName;
                bypassNGenAttributeDefaultConstructor = new MethodDefinition(".ctor", ctorAttributes, coreLibAssembly.MainModule.TypeSystem.Void);
#pragma warning disable RS0030 // Anything after MarkStep should use Cecil directly as all method bodies should be processed by this point
                var instructions = bypassNGenAttributeDefaultConstructor.Body.Instructions;
#pragma warning restore RS0030
                instructions.Add(Instruction.Create(OpCodes.Ldarg_0));
                instructions.Add(Instruction.Create(OpCodes.Call, systemAttributeDefaultConstructor));
                instructions.Add(Instruction.Create(OpCodes.Ret));

                bypassNGenAttributeDef.Methods.Add(bypassNGenAttributeDefaultConstructor);
            }
            else
            {
                foreach (MethodDefinition method in bypassNGenAttributeDef.Methods)
                {
                    if (method.IsConstructor && !method.IsStatic && !method.HasMetadataParameters())
                    {
                        bypassNGenAttributeDefaultConstructor = method;
                        break;
                    }
                }

                if (bypassNGenAttributeDefaultConstructor == null)
                {
                    throw new System.ApplicationException("System.Runtime.BypassNGenAttribute has no default constructor");
                }
            }

            MethodReference defaultConstructorReference = targetModule.ImportReference(bypassNGenAttributeDefaultConstructor);
            bypassNGenAttribute = new CustomAttribute(defaultConstructorReference);
        }
    }
}
